defmodule PsqlSchema do
	@moduledoc """
	    field 字段
	    primary_key 主键, {f1, f2}/f1; 如果是自增id, 则type为 bigserial
	    order_key 排序的字段
	    index 索引字段 index: [field, {field1, field2}]
	    unique 唯一性约束字段  unique: [field, {field1, field2}]
	    hyper 转化为时序数据库的字段
	    pipelines: 在执行deepr list, info 的时候, 可以对传入参数,处理结果进行进一步处理
			%{"list" => {mod, func, []}}
			def func(:before, user, args) do
			end

		default: 建表的时候设置字段的默认值
		json default:
			map: '{}'
			list: '[]'
	"""

	use SyntaxMacro


	defmacro schema(table, [do: block]) do
		quote do
			Module.put_attribute(__MODULE__, :table, unquote(table))
			Module.put_attribute(__MODULE__, :primary_key, nil)
			unquote(block)
		end
	end

	@doc """
	opts:
		null: 默认false,
				true, 表示可以为 null
				false, 不能为 null, not null
	"""
	defmacro field(field, type, opts \\ []) do
		opts = case Keyword.get(opts, :default) do
			nil ->
				opts
			v ->
				{v, _} = Code.eval_quoted(v)
				Keyword.put(opts, :default, v)
		end
		opts = case Keyword.get(opts, :mfa) do
			nil ->
				opts
			v ->
				{v, _} = Code.eval_quoted(v)
				Keyword.put(opts, :mfa, v)
		end
		default = Neo4jSchema.get_default_value(type, opts)
		info = MapLib.from_keyword(opts)
					 |> Map.put(:type, type)
		quote do
			(unquote(type) == :json || unquote(type) == :jsonb) &&
				Module.put_attribute(__MODULE__, :json, unquote(field))
			Module.put_attribute(__MODULE__, :field, unquote(Macro.escape({field, info})))
			Module.put_attribute(__MODULE__, :struct_field, unquote(Macro.escape({field, default})))
		end
	end


	defmacro mfa(mfa) do
		{mfa, _} = Code.eval_quoted(mfa)
		quote do
			Module.put_attribute(__MODULE__, :mfa, unquote(Macro.escape(mfa)))
		end
	end


	defmacro default(val) do
		quote do
			Module.put_attribute(__MODULE__, :default, unquote(val))
		end
	end

	defmacro json_desc(field, opts) do
		value = quote do
			from_keyword(unquote(opts))
		end
		quote do
			Module.put_attribute(__MODULE__, :json_desc, unquote({field, value}))
		end
	end

	defmacro foreign(field, mod, opts) do
		for_key = Keyword.get(opts, :for, nil)
		for_key == nil && raise TipError, "not set for key"
		v = Atom.to_string(for_key)
		opts = Keyword.put(opts, :for, v)
		value = Keyword.merge(opts, [mod: mod, type: :foreign])
		quote do
			Module.put_attribute(__MODULE__, :foreign, unquote({field, value}))
		end
	end

	# [{"list", {m, f, a}},{"insert", {m, f, a}}]
	@doc """
		deepr 有四个函数, insert, list, update, delete, 目前pipelines 只支持 list
		%{"list" => {mod, func, []}}, 执行deepr_list 的时候会调用 list 里面定义的 pipeline.
		拼接sql之前, 可以对 args 参数做一些参数(条件设置等)
		before 返回: args | {user, args} (可以修改user的值(project_id,获取不同项目的数据等))
		def func(:before, user, args) do
		end
		after返回说明:
		list 最终返回结果: %{object => %{"list" => rows, "total_page" => bool} = h}
		after 返回: {rows, map},
			map会和h merge(前端异步操作的时候,可以在里面放一些用于标志的)
		查询得到结果之后,可以对结果做一些处理(把用户id替换成名称等)
		def func(:after, user, rows) do
		end

	"""
	defmacro pipelines(pipes) do
		quote do
			Module.put_attribute(__MODULE__, :pipelines, unquote(pipes))
		end
	end

	defmacro is_global(bool) do
		quote do
			Module.put_attribute(__MODULE__, :is_global, unquote(bool))
		end
	end

	defmacro index(field) when is_tuple(field) do
		quote do
			Module.put_attribute(__MODULE__, :index, unquote(Macro.escape(field)))
		end
	end
	defmacro index(field) do
		quote do
			Module.put_attribute(__MODULE__, :index, unquote(field))
		end
	end

	defmacro hyper(field) do
		quote do
			Module.put_attribute(__MODULE__, :hyper, unquote(field))
		end
	end

	defmacro unique(field) when is_tuple(field) do
		quote do
			Module.put_attribute(__MODULE__, :unique, unquote(Macro.escape(field)))
		end
	end
	defmacro unique(field) do
		quote do
			Module.put_attribute(__MODULE__, :unique, unquote(Macro.escape(field)))
		end
	end

	defmacro primary_key(primary_key) when is_tuple(primary_key) do
		primary_key = Tuple.to_list(primary_key)
									|> Enum.map(&(Atom.to_string(&1)))
									|> :erlang.list_to_tuple()
		quote do
			Module.put_attribute(__MODULE__, :primary_key, Macro.escape(unquote(primary_key)))
		end
	end
	defmacro primary_key(primary_key) do
		quote do
			Module.put_attribute(__MODULE__, :primary_key, unquote(Atom.to_string(primary_key)))
		end
	end


	defmacro __before_compile__(_env) do
		quote unquote: false do
			fields = @field
							 |> Enum.reverse()
			json_descs = @json_desc
									 |> Neo4jSchema.from_keyword()
			foreign = @foreign
			{fields, proto_fields} = Enum.reduce(
				@field,
				{%{}, []},
				fn ({key, value}, {acc1, acc2}) ->
					json_desc = Map.get(json_descs, key)
					key = Atom.to_string(key)
					value = if json_desc do
						Map.put(value, :json_desc, json_desc)
					else
						value
					end
					{Map.put(acc1, key, value), [{key, value} | acc2]}
				end
			)
			{fields, proto_fields, foreign} = Enum.reduce(
				foreign,
				{fields, proto_fields, %{}},
				fn ({key, value}, {acc, acc2, acc3}) ->
					key = Atom.to_string(key)
					value = Neo4jSchema.from_keyword(value)
					acc3 = Map.put(acc3, key, value[:for])
					{Map.put(acc, key, value), [{key, value} | acc2], acc3}
				end
			)

			struct = @struct_field
							 |> Enum.reverse()
							 |> MapLib.list_to_map()
							 |> Map.put(:__struct__, __MODULE__)
			table = @table

			json = @json
						 |> Enum.map(&Atom.to_string(&1))

			default = @default
								|> Enum.map(&{Atom.to_string(elem(&1, 0)), elem(&1, 1)})
								|> Enum.into(%{})

			def primary_key() do
				unquote(Macro.escape(@primary_key))
			end

			def fields() do
				unquote(Macro.escape(fields))
			end

			def struct() do
				unquote(Macro.escape(struct))
			end

			def get_table() do
				unquote(table)
			end

			def json() do
				unquote(json)
			end

			index = Enum.map(@index, &Atom.to_string(&1))
			def index() do
				unquote(index)
			end

			def unique() do
				unquote(@unique)
			end

			def default() do
				unquote(Macro.escape(default))
			end

			def db_type() do
				Pango.DbType.pg()
			end

			def deepr_object() do
				get_table()
			end

			def pipelines() do
				unquote(Macro.escape(Neo4jSchema.from_keyword(@pipelines)))
			end

			def is_global() do
				unquote(@is_global)
			end

			def proto_fields() do
				unquote(Macro.escape(proto_fields))
			end

			def new(args) do
				MapLib.set_struct(args, __MODULE__)
			end

			def foreign() do
				unquote(Macro.escape(foreign))
			end

			length(@hyper) > 1 && throw({:error, "Hyper Length > 1"})
			hyper = Enum.map(@hyper, &Atom.to_string(&1))
			def hyper() do
				unquote(Macro.escape(hyper))
			end

			def mfa() do
				@mfa
			end

			defoverridable new: 1

		end

	end

	defmacro __using__(_options) do
		quote do
			import unquote(__MODULE__)
			Module.register_attribute(__MODULE__, :field, accumulate: true)
			Module.register_attribute(__MODULE__, :json, accumulate: true)
			Module.register_attribute(__MODULE__, :json_desc, accumulate: true)
			Module.register_attribute(__MODULE__, :foreign, accumulate: true)
			Module.register_attribute(__MODULE__, :struct_field, accumulate: true)
			Module.put_attribute(__MODULE__, :index, [])
			Module.put_attribute(__MODULE__, :mfa, nil)
			Module.put_attribute(__MODULE__, :hyper, [])
			Module.put_attribute(__MODULE__, :unique, [])
			Module.put_attribute(__MODULE__, :pipelines, [])
			Module.put_attribute(__MODULE__, :default, %{})
			Module.put_attribute(__MODULE__, :primary_key, nil)
			Module.put_attribute(__MODULE__, :is_global, false)
			@before_compile unquote(__MODULE__)
		end
	end


	def get_sql(mod, prefix) do
		{table, constraints_name} =
			if prefix != nil && !mod.is_global() do
				{inspect(prefix) <> "." <> mod.get_table(), prefix <> "_" <> mod.get_table()}
			else
				{mod.get_table(), mod.get_table()}
			end
		fields = mod.fields()
		[concat_sql(mod, table, constraints_name)] ++
		Enum.map(
			mod.index(),
			fn
				({k, v}) ->
					{k1, v1} = if Map.get(fields, k).type == :date do
						{k <> " DESC", v}
					else
						if Map.get(fields, v).type == :date do
							{k, v <> " DESC"}
						else
							{k, v}
						end
					end
					"""
					CREATE INDEX ind_#{constraints_name}_#{k}_#{v} ON #{table}(#{k1}, #{v1});
					"""
				(k) ->
					k1 = if Map.get(fields, k).type == :date do
						k <> " DESC"
					else
						k
					end
					"""
					CREATE INDEX ind_#{constraints_name}_#{k} ON #{table}(#{k1});
					"""
			end
		)
		++ ["ALTER TABLE #{table} OWNER to postgres;"] ++
			 Enum.map(
				 mod.hyper(),
				 &("SELECT create_hypertable('#{table}', '#{&1}');")
			 )
	end

	def create_table(mod, schemas) when is_list(schemas) do
		Enum.each(schemas, &create_table(mod, &1))
	end
	def create_table(mod, prefix) do
		get_sql(mod, prefix)
		|> Enum.each(
				 fn (sql) ->
					 Process.sleep(20)
					 PsqlDb.query(sql)
				 end
			 )
	end



	def concat_sql(mod, table, constraints_name) do
		filed_sql =
			Enum.filter_map(
				mod.proto_fields(),
				&(not (elem(&1, 1)[:type] == :foreign || Map.get(elem(&1, 1), :lack) == true)),
				fn ({field, field_info}) ->
					get_field_sql(mod, field, field_info)
				end
			) ++
			Enum.map(
				mod.unique(),
				fn
					v when is_tuple(v) ->
						list = Tuple.to_list(v)
						uq = Enum.join(list, "_")
						u = Enum.join(list, ",")
						"CONSTRAINT uq_#{constraints_name}_#{uq} UNIQUE(#{u})"
					k ->
						"CONSTRAINT uq_#{constraints_name}_#{k} UNIQUE(#{k})"
				end
			)
			|> Enum.join(", ")
		primary_key_str = if mod.hyper() == [] do
			primary_key_sql(mod.primary_key())
		else
			""
		end
		"""
		CREATE TABLE IF NOT EXISTS #{table}(#{filed_sql}#{primary_key_str})
		WITH (
		  OIDS = FALSE
		)
		TABLESPACE pg_default;
		"""
	end

	def primary_key_sql({f1, f2}) do
		", PRIMARY KEY(#{f1}, #{f2})"
	end
	def primary_key_sql(nil) do
		""
	end
	def primary_key_sql(f) do
		", PRIMARY KEY(#{f})"
	end

	def get_field_sql(mod, field, field_info) do
		%{:type => type} = field_info
		(type != :bigserial || type == :bigserial && mod.primary_key == field) || SyntaxMacro.remind(__ENV__)
		str = if field in mod.hyper() do
			" NOT NULL "
		else
			""
		end
		defaults = mod.default()
		default = case Map.get(field_info, :default) do
			nil ->
				get_default(defaults, field)
			v ->
				cond do
					is_list(v) ->
						" DEFAULT '[]'"
					is_map(v) ->
						" DEFAULT '{}'"
					true ->
						" DEFAULT #{v}"
				end
		end
		not_null = case Map.get(field_info, :null) do
			true ->
				""
			_ ->
				" NOT NULL"
		end
		"#{field} " <> convert_type(type) <> str <> default <> not_null
		|> IO.inspect()
	end


	def get_default(defaults, field) do
		v = Map.get(defaults, field)
		if v != nil do
			s = cond do
				is_map(v) ->
					"'#{Jason.encode!(%{})}'"
				is_list(v) ->
					"'[]'"
				true ->
					throw({:error, defaults, field, "error"})
			end
			" DEFAULT #{s}"
		else
			""
		end
	end

	def convert_type(:bigserial) do
		"bigserial"
	end
	def convert_type(:integer) do
		"integer"
	end
	def convert_type(:int8) do
		"int8"
	end
	def convert_type(:string) do
		"varchar(255)"
	end
	def convert_type(:money) do
		"numeric(10, 2)"
	end
	def convert_type(:json) do
		"json"
	end
	def convert_type(:jsonb) do
		"jsonb"
	end
	def convert_type(:date) do
		"date"
	end
	def convert_type(:timestamp) do
		"timestamp(6) WITHOUT time zone"
	end
	def convert_type(:boolean) do
		"boolean"
	end
	def convert_type(:float) do
		"real"
	end
	def convert_type({:array, :string}) do
		"varchar(255)[]"
	end
	def convert_type({:array, :integer}) do
		"integer[]"
	end
	def convert_type(type) do
		throw({:error, type})
	end


	def from_keyword(keyword) when is_list(keyword) do
		v = from_keyword(hd(keyword))
		[v]
	end
	def from_keyword(keyword) do
		Enum.reduce(
			keyword,
			%{},
			fn ({key, value}, acc) ->
				Map.put(acc, key, value)
			end
		)
	end

end
